import logging

import io.ioloop
import util.common

log = logging.getLogger('channel')

#------------------------------------------------------------------------------
class Channel:
    """Represents an open physical communications channel (e.g. TCP
    connection, present USB key). Handles the intricacies of dealing
    with the lower layer such as handshakes, connection checking.
    
    Abstract base class."""

    def __init__(self, tx, iface_id, id):
        self.peer_comms_    = {}
        self.cur_peer_comm_ = 0
        self.iface_id_      = iface_id
        self.channel_id_    = id
        self.tx_            = tx

        assert self not in self.tx_.channels_
        self.tx_.channels_.append(self)

    def type(self):
        """The type of channel this is (TCP, SMS, etc)"""
        raise NotImplementedError

    def summary(self):
        """Return a pair (type, desc) describing the channel
        XXX/bowei - might need more information?"""
        raise NotImplementedError()

    def iface_id(self):
        return self.iface_id_

    def channel_id(self):
        return self.channel_id_
    
    def close(self):
        """Close this channel. Deregisters the channel from any open
        PeerComms"""
        for k in self.peer_comms_.keys():
            self.peer_comms_[k].deregister()
        self.peer_comms_ = {}
        assert self in self.tx_.channels_
        self.tx_.channels_.remove(self)

    def add_peer_comm(self, id, c):
        """Add a peer comm to the channel"""
        log.debug('%s: added peer_comm[%s] = %s' % (self, id, c))
        self.peer_comms_[id] = c

    def send_data(self, id, data):
        """Send a single unit of application data"""
	raise NotImplementedError()

    def signal_data_req(self):
        """Signal to a peer that data_req is ready"""
        # XXX/bowei -- more efficient rotation code
        self.cur_peer_comm_ = (self.cur_peer_comm_ + 1) % len(self.peer_comms_)
        for pc in (self.peer_comms_.keys()[:self.cur_peer_comm_] +
                   self.peer_comms_.keys()[self.cur_peer_comm_:]):
            if not self.peer_comms_[pc].req_pending():
                self.peer_comms_[pc].signal_req_pending()                
                return
        log.debug('Trying to signal data ready, but none available')

#------------------------------------------------------------------------------
class Tcp(Channel, io.ioloop.SockHandle):
    """TCP channel."""

    PREHS     = 'prehandshake'
    HANDSHAKE = 'handshake'
    CONNECTED = 'connected'
    CLOSED    = 'closed'

    # Class variable stores the id for each connection
    id = 0

    class ConnectFactory(io.ioloop.ListenFactory):
        """Handles creating channels upon connection for listening TCP
        sockets"""
        def __init__(self, tx, iface_id):
            self.tx_ = tx
            self.iface_id_ = iface_id
        
        def make_handle(self, sock):
            """this will register the channel with io.ioloop. since we
            haven't gotten the handshake from the peer, we can't
            register this with a peer yet."""
            return Tcp(tx            = self.tx_,
                       sock          = sock,
                       iface_id      = self.iface_id_,
                       send_id_first = False)
        
    class HsState:
        """A handshake is done when we have both sent and received the
        hs"""
        def __init__(self):
            self.state_ = 0
        def our_shake(self):
            self.state_ = self.state_ | 1
        def their_shake(self):
            self.state_ = self.state_ | 2
        def is_done(self):
            return self.state_ == 3

    def __init__(self, tx, sock, iface_id, send_id_first,
                 close_hook = None):
        """close_hook is a thunk run at socket.close time"""
        Channel.__init__(self, tx, iface_id, Tcp.id)        
	Tcp.id = Tcp.id + 1

        io.ioloop.SockHandle.__init__(self, sock)
        
        self.state_ = Tcp.PREHS
        self.hs_state_ = Tcp.HsState()
        self.remote_id_ = None
        self.close_hook_ = close_hook
        
        # if we are to initiate the exchange, send our id
        if send_id_first:
            self.send_id()
        self.initiator_ = send_id_first

        
    def type(self):
        return 'tcp'

    def summary(self):
	# XXX/bowei this is kind of hackish, changeme
        return ('tcp', self.sock_.getpeername())

    def send_id(self):
        """Send connection id handshake to remote host"""
        self.send_frame(self.tx_.neon_id()) 
        
    def recv_frame(self, msg):
        """Dispatch routine for receiving data."""
        try:
            if self.state_ == Tcp.PREHS:
                self.state_prehs(msg)
            elif self.state_ == Tcp.HANDSHAKE:
                self.state_hs(msg)
            elif self.state_ == Tcp.CONNECTED:
                self.state_con(msg)
            elif self.state_ == Tcp.CLOSED:
                log.debug('dropping message because we are closed')
        except ValueError:
            Log.error('Error in the protocol')
        
    def state_prehs(self, msg):
        """State before receiving an identifying signature on from the
        other end"""
        # XXX/bowei handle race condition
        if not self.initiator_:
            self.send_id()
        self.remote_id_ = msg.strip()
        if len(self.remote_id_) < 1:
            log.warn('other side sent garbage for id')
            self.close()
            return
        log.debug('%s connected to %s' % (self, self.remote_id_))
        self.set_state(Tcp.HANDSHAKE)
        p = self.tx_.get_peer(self.remote_id_)
        pc = p.reg_comm(self)
        self.add_peer_comm(id, pc)
        pc.signal_hs_req_pending()

    def state_hs(self, msg):
        """State in which this channel is exchanging application
        handshakes"""
        log.info('Got handshake from %s: %s' % (self.remote_id_, msg))
        if len(msg) < 1 or msg[0] != 'h':
            log.error('in hs state, but got garbage "%s", closing' % msg)
            self.close()
        _, peer_id, hs = msg.split(' ', 2)

        found = False
        for k in self.peer_comms_.keys():
            if (self.peer_comms_[k].peer_id() == peer_id):
                self.peer_comms_[k].recvd_handshake(hs)
                found = True
                
        if not found:
            log.warn('Handshake for %s, but no peer_comm matches' % peer_id)
        self.hs_state_.their_shake()
        
        if self.hs_state_.is_done():
            self.set_state(Tcp.CONNECTED)
            self.signal_data_req()
            
    def state_con(self, msg):
        """State after handshakes have been exchanged. Data exchange
        state."""
        if len(msg) < 1 or msg[0] != 'd':
            log.error('in conn state, but got garbage "%s", closing' % msg)
            self.close()
        _, peer_id, data = msg.split(' ', 2)
        for k in self.peer_comms_.keys():
            if (self.peer_comms_[k].peer_id() == peer_id):
                self.peer_comms_[k].recvd_data(data)
                # XXX/bowei -- should deliver to all, or only the first
                return
        log.warn('no available peer to deliver to: %s' % peer_id)
        
    def close(self):
        """Close the channel and cleanup state associated with the
        channel"""
        log.debug('Closing channel %s' % self)
        util.common.dtest('tx', 'channel:%s/%d:%s:closed' %
                          (self.type(), self.channel_id(), self.remote_id_))
        Channel.close(self)
        io.ioloop.SockHandle.close(self)
        io.ioloop.master_loop.remove_handle(self)

        if self.close_hook_ is not None:
            self.close_hook_()
        
    def set_state(self, state):
        log.debug('Tcp state %s -> %s' % (self.state_, state))
        util.common.dtest('tx', 'channel:%s/%d:%s:%s' %
                          (self.type(), self.channel_id(),
                           self.remote_id_, state))
        self.state_ = state

    def send_handshake(self, peer_id, hs):
        """Send a application handshake to the other side."""
        log.debug('%s: sending handshake "%s"' % (self, hs))
        self.send_frame('h %s %s' % (peer_id, hs))
	self.hs_state_.our_shake()
        if self.hs_state_.is_done():
	    self.set_state(Tcp.CONNECTED)
            self.signal_data_req()

    def send_data(self, peer_id, data):
        """Send a single unit of application data"""
        log.debug('%s: sending data "%s"' % (self, data))
        self.send_frame('d %s %s' % (peer_id, data))

    def handle_write(self):
        """Virtual from io.ioloop.SockHandle. We need to issue a
        req_pending to the client when we are in the connected
        state"""
	io.ioloop.SockHandle.handle_write(self)
	if self.write_buf_ == '' and self.state_ == Tcp.CONNECTED:
	    log.debug('write buffer empty, upcall for data')
            self.signal_data_req()
            
#------------------------------------------------------------------------------
class Fs(Channel):
    """File system channel."""

    def __init__(self, name):
        raise NotImplementedError()
    
    def type(self):
        return 'fs'

    def close(self):
        raise NotImplementedError()

#------------------------------------------------------------------------------
class Sms(Channel, io.ioloop.SockHandle):
    """SMS channel"""

    def __init__(self, tx, iface_id, sms_method, sock):
        Channel.__init__(self, tx, iface_id, 'sms')
        self.sms_method = sms_method
        self.sock_      = sock
        
    def type(self):
        return 'sms'

    def summary(self):
        # XXX/bowei -- what to return in summary?
        return ('sms', None)

    def close(self):
        log.warn('sms channel should not be closed, ignoring')

    def send_data(self, id, data):
        """Send a single unit of application data"""
        log.debug('%s: sending data "%s"' % (self, data))
        self.send_frame('d %s %s' % (id, data))

    def recv_frame(self, frame):
        if frame[0] == 's':
            _, num, text = frame.split(' ', 2)
            peer_id = self.sms_method.get_alias(num)
            if not peer_id:
                log.warn('received sms from unknown #: %s, ignoring' % num)
                return
            log.debug('received sms from %s: %s' % (peer_id, text))
            for k in self.peer_comms_.keys():
                if (self.peer_comms_[k].peer_id() == peer_id):
                    self.peer_comms_[k].recvd_data(data)
                    # XXX/bowei -- should deliver to all or only the first
                    return
            log.warn('no available peer to deliver to: %s' % peer_id)
        else:
            log.error('unknown message type: %s, ignoring' % frame)
            # XXX/bowei -- disconnect from SMS?
        
